#include "RayTracer.h"

#include <math.h>
#include <opencv/highgui.h>

#include "../Global.h"
#include "../object/Sphere.h"
#include "../object/Plane.h"
#include "../object/Mesh.h"

#include "../math/Ray.h"
#include "../math/Vector.h"

#include "../light/Light.h"

#include "../helper/Helper.h"

Light** lights;
int numLights;

Light lightWhite;
Light lightLeft;
Light lightRight;

Sphere sphere;
Plane planeLeft;
Plane planeBack;
Plane planeRight;
Plane planeTop;
Plane planeBottom;

Mesh triangle;

Vector3f ambientColor;
Vector3f diffuseColor;
Vector3f specularColor;

RayTracer::RayTracer()
{
	camera = Global::GetGlobal()->GetCamera();
	/* save width, height */
	width = 640;
	height = 480;
	bpp = 3; //use default RGB color space
	
	/* allocate internal buffer */
	rayImage = cvCreateImage(cvSize(width, height), 8, bpp);
	rayPixels = (byte*)rayImage->imageData;
	floatPixels = new float[width*height*bpp];
	
	/* 1. eye position */
	eye = Vector3f(0, 0, 10);
	/* 2. field of view */
	ComputeFieldOfView(); //field of view based on eye position
	
	/* frame id */
	frameId = 0;
	
	
	/* scene objects */	
	sphere.radius = 15;
	sphere.pos = Vector3f(0, 0, -30);
	sphere.material = Material(0.1, 0.7, 0.5, 51);
	sphere.material.color = Vector3f(1.0f, 1.0f, 0.0f);
	sphere.material.reflection = 0.0f;
	
	planeLeft.pos = Vector3f(-30, 0, 0);
	planeLeft.normal = Vector3f(1, 0, 0);
	planeLeft.material = Material(0.1, 0.8, 0.5, 11);
	planeLeft.material.color = Vector3f(0.0f, 1.0f, 0.0f);
	planeLeft.material.reflection = 0.5f;
		
	planeBack.pos = Vector3f(0, 0, -160);
	planeBack.normal = Vector3f(0, 0, 1);
	planeBack.material = Material(0.1, 0.8, 0.5, 11);
	planeBack.material.color = Vector3f(0.5f, 0.5f, 0.5f);
	
	planeRight.pos = Vector3f(30, 0, 0);
	planeRight.normal = Vector3f(-1, 0, 0);
	planeRight.material = Material(0.1, 0.8, 0.5, 11);
	planeRight.material.color = Vector3f(1.0f, 0.0f, 0.0f);
	planeRight.material.reflection = 0.5f;
	
	planeTop.pos = Vector3f(0, 50, 0);
	planeTop.normal = Vector3f(0, -1, 0);
	planeTop.material = Material(0.1, 0.8, 0.5, 11);
	planeTop.material.color = Vector3f(0.5f, 0.5f, 0.5f);
	//planeTop.material.reflection = 0.3f;
	
	planeBottom.pos = Vector3f(0, -50, 0);
	planeBottom.normal = Vector3f(0, 1, 0);
	planeBottom.material = Material(0.1, 0.8, 0.5, 11);
	planeBottom.material.color = Vector3f(0.5f, 0.5f, 0.5f);
	//planeBottom.material.reflection = 0.3f;
	
	Vector3f tri[] = {Vector3f(-25, -10, -50), Vector3f(25, -10, -50), Vector3f(0, 25, -50)};
	triangle.Load(tri, 3);
	triangle.material = Material(0.1, 0.8, 0.1, 11);
	triangle.material.color = Vector3f(1, 0, 1);
	
	
	lightWhite.pos = Vector3f(30, 0, eye.z);
	//light.pos = eye;
	lightWhite.ambient = Vector3f(1, 1, 1);
	lightWhite.diffuse = Vector3f(1, 1, 1);
	lightWhite.specular = Vector3f(1, 1, 1);
	
	lightLeft.pos = Vector3f(-40, 30, -20);
	lightLeft.ambient = Vector3f(0.5, 0.5, 0.5);
	lightLeft.diffuse = Vector3f(0, 1, 0);
	lightLeft.specular = Vector3f(1, 1, 1);
	
	lightRight.pos = Vector3f(40, 30, -20);
	lightRight.ambient = Vector3f(0.5, 0.5, 0.5);
	lightRight.diffuse = Vector3f(1, 0, 0);
	lightRight.specular = Vector3f(1, 1, 1);
	
	numLights = 3;
	lights = new Light*[10];
	lights[0] = &lightWhite;
	lights[1] = &lightLeft;
	lights[2] = &lightRight;
	
	
	objectCount = 6;
	objects = new Object*[10];
	objects[3] = &planeBack;
	objects[1] = &planeLeft;	
	objects[2] = &planeRight;
	objects[0] = &sphere;
	objects[4] = &planeTop;
	objects[5] = &planeBottom;
	
	objects[6] = &triangle;
}

RayTracer::~RayTracer()
{
	cvReleaseImage(&rayImage);
	ReleaseNewArray(objects);
	ReleaseNewArray(lights);
	ReleaseNewArray(floatPixels);
}

void RayTracer::ComputeFieldOfView() {
	/* field of view */
	fovX = M_PI/4;
	fovY = height * fovX / width; /* approximation */
	/* the distance from the eye to the view plane */
	zNear = -1;
	
	/* view plane width and height */
	float y = tan(fovX/2)*abs(zNear);
	viewPlaneHeight = 2*y;
	float x = width* y / height;
	viewPlaneWidth = 2*x;	
	printf("View plane: (%.2f, %.2f)\n", viewPlaneWidth, viewPlaneHeight);
		
	/* anchor point for iteration during ray tracing */
	float mx = -viewPlaneWidth / 2;
	float my = viewPlaneHeight / 2;
	float mz = eye.z + zNear;
	topleftAnchor = Vector3f(mx, my, mz);
	dx = viewPlaneWidth / width;
	dy = -viewPlaneHeight / height;
}


float min = 255, max = -255;
int firstFrame = 1;

void RayTracer::Trace(Ray3f ray, Vector3f& vColor, int depth) {
	if (depth >= RAY_TRACE_MAX_DEPTH) {
		vColor.Assign(0, 0, 0);
		return;
	}
	
	int sol;
	//Vector3f hit[2];
	HitInfo hit[10];
	Vector3f nnear(0, 0, 0);
	Vector3f far(0, 0, 0);
	
	Vector3f vAmbient;
	Vector3f vDiffuse;
	Vector3f vSpecular;
	
	Vector3f reflection;
	Vector3f normal;
	Vector3f lightDir;
	
	/* find intersection */
	//ray.IntersectSphere(sphere.pos, sphere.radius, sol, near, far);
	//sphere.TestHit(ray, hit, sol);
	HitInfo minHit;
	int firstHit = 0;
	Material material;
	int k;
	for (k = 0; k < objectCount; k++) {
		sol = objects[k]->TestHit(ray, hit, sol);
		if (sol > 0) {
			if (firstHit == 0) {
				minHit = hit[0];
				material = objects[k]->material;
				
				firstHit = 1;						
			} else if (minHit.point.z < hit[0].point.z) {
				minHit = hit[0]; //save hit info
				material = objects[k]->material; //save material
			}
		}
	}
	
	/* ensure hit point inside the view frustum */
	if (minHit.point.z > topleftAnchor.z) //out of view
		firstHit = 0;
				
	/* calculate color */
	if (firstHit) {
		
		//printf("hit\n");
		//near = hit[0];
		/* use near as the visible point */	
		//normal = (near - sphere.pos).Normalize();
		nnear = minHit.point;
		normal = minHit.normal;
		/* ray reflection */
		reflection = (ray.direction - normal*2*(ray.direction.Dot(normal))).Normalize();		
		
		/* iterate all light sources */
		for (int i = 0; i < numLights; i++) {			
			/* ambient light */
			ambientColor = lights[i]->ambient.Multiply(material.color);
			vAmbient += ambientColor * material.ambient;
			/* light direction */
			lightDir.Assign((lights[i]->pos - nnear).Normalize());
			/* === Phong shading === */		
			/* diffuse */			
			float ln = lightDir.Dot(normal);
			if (ln < 0) { //the light point to the back of the object
				vDiffuse += Vector3f(0, 0, 0); 			
				vSpecular += Vector3f(0, 0, 0);
			} else {
				diffuseColor = lights[i]->diffuse.Multiply(material.color);
				vDiffuse += diffuseColor* ln * material.diffuse;
				
				/* specular */
				specularColor = lights[i]->specular.Multiply(material.color);
				float alpha = material.phongShininess;			
				/* phong specular */
				/*Vector3f lightReflection = Vector3f::Reflect(-lightDir, normal).Normalize();
				float rv = (-ray.direction).Dot(lightReflection); //dot product of the view vector and the reflection vector
				vSpecular = specularColor*(pow(rv, alpha)) * material.specular;*/
				
				/* blinn-phong specular for better performance since we skip the calculation of the reflection vector */
				alpha = material.phongShininess*2;
				Vector3f h = (lightDir - ray.direction).Normalize();
				float nh = normal.Dot(h);
				vSpecular += specularColor*(pow(nh, alpha)) * material.specular;
			}
		}
		/* reflection */
		Vector3f reflectColor(0, 0, 0);
		if (material.reflection > 0) {			
			Ray3f reflectRay(nnear, reflection);
			Trace(reflectRay, reflectColor, depth+1);
		}
		/* final intensity */		
		vColor = vAmbient + vDiffuse + vSpecular + reflectColor * material.reflection;
	} else {
		/* just a black background */
		vColor = Vector3f(0, 0, 0);
	}
}

void RayTracer::RenderSphere() {
	/* a simple sphere scene */		
	Vector3f vColor;
	Color3f fColor;
	Color3i iColor;
	
	
	
//	int w2 = width/2;
//	int h2 = height/2;	
	
	float sx = topleftAnchor.x; /* start x */
	float sy = topleftAnchor.y; /* start y */
	
	/* for each pixels */
	int i, j, k, index;
	for (i = 0; i < height; i++) {
		for (j = 0; j < width; j++) {
			/* create a ray */			
			ray.point = eye;
			//ray.direction = (Vector3f((j - w2)*tan(fovX), (h2 - i)*tan(fovY), 0) - eye).Normalize();
			//ray.direction = (Vector3f((j - w2)*viewPlaneWidth/width, (h2 - i)*viewPlaneHeight/height, eye.z + zNear) - eye).Normalize();
			Vector3f dir (sx - eye.x, sy - eye.y, /* eye.z + zNear - eye.z */ zNear); 
			//printf("%f %f\n", sz, eye.z);
			//Vector3f dir (Vector3f(sx, sy, sz) - eye);
			ray.direction = dir.Normalize();
			
			Trace(ray, vColor, 0);
			
//			char info[256];
//			memset(info, 0, 256);
//			sprintf(info, "Direction: (%f, %f, %f)\n", dir.x, dir.y, dir.z);
//			Helper::Log(info);
			//orthogonal ray tracing
			//ray.direction = Vector3f(0, 0, -1);
			//ray.point = Vector3f(j-w2, h2-i, 25);
			
			
				//fColor = Color3f(vColor.x/2 + 0.5, vColor.y/2 + 0.5, vColor.z/2 + 0.5);
				//fColor = Color3f((vColor.x + 6) / 8, (vColor.y + 6) / 8, (vColor.z + 6) / 8);
				//float s = vColor.x + vColor.y + vColor.z;
				//fColor = Color3f(vColor.x / s, vColor.y / s, vColor.z / s); //normalized color
			/* map to [0, 1] */
			fColor = Color3f(vColor.x, vColor.y, vColor.z);
			
			
			if (fColor != Color3f(0, 0, 0)) {
//				char info[256];
//				memset(info, 0, 256);
//				sprintf(info, "Color: (%f, %f, %f)\n", fColor.red, fColor.green, fColor.blue);
				//Helper::Log(info);
				//sprintf(info, "Direction: (%f, %f, %f)\n", (-ray.direction).x, (-ray.direction).y, (-ray.direction).z);
				//Helper::Log(info);
			}
			
		
			
			/* clamp fColor to [0, 1] */
			//Helper::Clamp(fColor);
			
			/* save image, note that we need to swap to be compatible with the BGR color model of OpenCV */			
			
			index = (i*width+j)*bpp;
			floatPixels[index  ] = fColor.blue;
			floatPixels[index+1] = fColor.green;
			floatPixels[index+2] = fColor.red;		

			
			/* update sx */
			sx += dx;
		}
		/* update sy */
		sy += dy;
		sx = topleftAnchor.x;
	}
	
	/* map float color to [0, 255] */
	/*float max[] = {-255, -255, -255};
	float min[] = {255, 255, 255};*/
	//if (firstFrame) {
	//	firstFrame = 0;
		for (k = 0; k < bpp; k++)
			for (i = 0; i < height; i++) {
				for (j = 0; j < width; j++) {
					index = (i*width+j)*bpp+k;
					/*if (floatPixels[index] > max[k]) max[k] = floatPixels[index];
					if (floatPixels[index] < min[k]) min[k] = floatPixels[index];*/
					if (floatPixels[index] > max) max = floatPixels[index];
					if (floatPixels[index] < min) min = floatPixels[index];		
				}
			}
	//}
	
	for (k = 0; k < bpp; k++)
		for (i = 0; i < height; i++) {
			for (j = 0; j < width; j++) {
				index = (i*width+j)*bpp + k;
				//rayPixels[index] = 255 * (floatPixels[index] - min[k]) / (max[k] - min[k]);
				rayPixels[index] = 255 * (floatPixels[index] - min) / (max - min);
				/*if (floatPixels[index] < 0)
					floatPixels[index] = 0;
				if (floatPixels[index] > 1)
					floatPixels[index] = 1;
				rayPixels[index] = 255*floatPixels[index]; 		*/
			}
		}
		
		

}

void RayTracer::Update(int time) {	
	//sphere.pos.y++;
	//light.pos.y++;
	sphere.pos.z += -1;
}

void RayTracer::Render() {
	RenderSphere();
}

void RayTracer::OpenWriter(const char* file, float fps, int width, int height) {
	writer = cvCreateVideoWriter(file, CV_FOURCC('P','I','M','1'), fps, cvSize(width, height));

}

void RayTracer::CloseWriter() {
	cvReleaseVideoWriter(&writer);
}

void RayTracer::WriteFrame() {
	cvWriteFrame(writer, rayImage);
}

void RayTracer::WriteImage() {
	/* save image */
	char file[100];
	memset(file, 0, 100);
	sprintf(file, "sphere%3d.bmp", frameId++);
	sprintf(file, "sphere(%.0f#%.0f#%.0f).bmp", sphere.pos.x, sphere.pos.y, sphere.pos.z);
	cvSaveImage(file, rayImage);
}


void RayTracer::OnMainLoop() {
	Update(timer.Update());
	Render();	
}

void RayTracer::Main() {
	/* start timer */
	timer.Start();
	/* save image */
	OnMainLoop();
	WriteImage();
	
	/* save video */
	float fps = 30;
	int length = 2; //seconds
	int frames = (int)(fps*length);
	OpenWriter("sphere3.mpg", fps, width, height);
	for (int i = 0; i < frames; i++) {
		printf("Frame %i at speed %.2f, average speed: %.2f:\n", i, timer.GetFps(), timer.GetAverageFps());
		OnMainLoop();
		WriteFrame();
	}
	CloseWriter();
	
	/* time elapsed */
	/* update timer again for accurate result */
	timer.Update();
	
	char elapsed[100];
	sprintf(elapsed, "Time elapsed: %s\n", timer.ToString());
	Helper::Log(elapsed);
	
	/* stop timer */
	timer.Stop();
}

